Skip to content

MPDX-9818 - Add DA5 and A33 warnings/errors to goal settings - #1930

Open
zweatshirt wants to merge 4 commits into
mainfrom
MPDX-9818
Open

MPDX-9818 - Add DA5 and A33 warnings/errors to goal settings#1930
zweatshirt wants to merge 4 commits into
mainfrom
MPDX-9818

Conversation

@zweatshirt

@zweatshirt zweatshirt commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

Description

We want to show a warning to the admin when the salary the admin has entered is over the salary cap.
Backend PR: https://github.com/CruGlobal/mpdx_api/pull/3469

Testing

  • Go to the GoalSettingsForm
  • Enter a large amount for 'Annual Requested Salary'
  • Check that the warning displays that the amount exceeds the salary cap.

Checklist:

  • I have given my PR a title with the format "MPDX-(JIRA#) (summary sentence max 80 chars)"
  • I have applied the appropriate labels (Add the label "Preview" to automatically create a preview environment)
  • I have run the Claude Code /pr-review command locally and fixed any relevant suggestions
  • I have requested a review from another person on the project
  • I have tested my changes in preview or in staging
  • I have cleaned up my commit history

@zweatshirt zweatshirt self-assigned this Jul 20, 2026
@zweatshirt zweatshirt added Preview Environment Add this label to create an Amplify Preview Staging API Run GraphQL codegen against the staging API labels Jul 20, 2026
@github-actions

Copy link
Copy Markdown
Contributor

Preview branch generated at https://MPDX-9818.d3dytjb8adxkk5.amplifyapp.com

@github-actions

github-actions Bot commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

Bundle sizes [mpdx-react]

Compared against ea57016

Route Size (gzipped) Diff
/accountLists/[accountListId]/coaching/[coachingId]/nsGoalCalculator 243.45 KB +1.08 KB
/accountLists/[accountListId]/hrTools/mpdGoalAdmin/scenario/[scenarioGoalId] 243.44 KB +1.08 KB

@zweatshirt zweatshirt removed the Staging API Run GraphQL codegen against the staging API label Jul 21, 2026

@zweatshirt zweatshirt left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Multi-Agent Code Review — PR #1930

Verdict: APPROVED WITH SUGGESTIONS — no blockers. Highest finding is 6.5/10.

Risk: 7/10 (HIGH)src/components/**/*.graphql (+2), 4x feature components (+4), 146 non-test lines (+1). Senior review recommended. Codegen verified clean (yarn gql succeeds, yarn lint:ts exits 0, 213 tests pass).

Two rounds: 6 specialized agents each. Round 2 was re-run with round-1 findings adjudicated against verified facts, which recalibrated three findings downward and surfaced two better ones.

Key recalibrations from round 1

Finding Was Now Why
Failed preview silently suppresses warning 7.5 3.5 Self-corrects on save (UpdateNewStaffGoalCalculation returns the fragment incl. salaryOverCap; enableReinitialize set; Save never gated on it)
Duplicate preview mutations 7.5 6.5 BatchHttpLink (batchInterval 20ms) coalesces both into one HTTP request; cost is doubled server compute, not round-trips
savedSalaryOverCap = false fails open 4.5 2.0 The only caller omitting it (MpdGoalPreview) never reads salaryOverCap

Verified correct (no action)

  • The always-mounted empty aria-live region is proper live-region technique — mutation-tested: deleting aria-live and making the region conditional both fail the suite.
  • Cache normalization correct (NewStaffGoalCalculationCalculations has no id; non-normalizable value type). fetchPolicy: 'no-cache' correct (NewStaffGoalCalculationPreview is a distinct typename).
  • Destructuring-with-defaults change is behaviourally identical (monthlyGoal: Float!, salaryOverCap: Boolean!).
  • Scenario-goal path (accountListId: null) is valid per schema and pre-existing via MpdGoalPreview.
  • Save-while-preview-in-flight race traced and safe.
  • Cap semantics: a non-joining senior-staff spouse's salary IS included against the married cap (applicable? = !spouse? || !single?) — coherent, no bug. SOSA correctly treated as single.
  • Mock fixtures deterministic (seed = 'seed', probed across processes).
  • salaryOverCap: false added to MpdGoalPreview.test.tsx is inert — removing it still passes 15/15.
  • Missing public/locales/en key follows the repo's batch-extraction convention. Not a violation.

Findings outside this PR's diff (informational, not blockers)

1. The contrast fix belongs in src/theme.ts, not in this component. src/theme.ts never defines palette.warning, so warning.main resolves to MUI's default #ed6c02 (3.11:1 on #fff). But HrTools uses warning.main in 7 of 7 pre-existing cases — changing only this file would make it the sole diverging file. Systemic fix:

warning: { main: statusColors.warning },  // #D34400 -> 4.57:1
success: { main: statusColors.success },  // CardTitle.tsx:24 has the same latent defect

Pre-existing HrTools-wide. Recommend a standalone ticket rather than blocking this PR.

2. Review-process gap. .claude/rules/code-review.md lists src/components/User/impersonate*, which matches nothing — impersonation lives in Settings/Admin/ImpersonateUser/, Settings/Organization/ImpersonateUser/, and ProfileMenu/. The Security agent's impersonation trigger never fires. Also stale: src/components/Reports/SalaryCalculator/** and pages/api/Schema/uploads/** don't exist.

3. Doc drift. CLAUDE.md and the rules file say "Material UI v5"; yarn.lock resolves @mui/material 7.3.11.

4. Questions for the API team (mpdx_api #3469):

  • salary_over_cap? calls base_salary_cap unconditionally, whereas salary_cap only reaches it when allow_salary_over_cap == 'no'. It resolves via find_by!, which raises. Are the four board_approved_salary_calc misc constants seeded for every historical calculations_year? A raise would null the whole calculations block via non-null propagation, taking monthlyGoal down with it.
  • salary_over_cap? uses strict > — confirm equal-to-cap is intended as compliant.

Recommended order of work

  1. The flicker fix (useMpdGoalPreview.ts:151) — one line, real user-facing defect
  2. Two test assertions (warning renders in the form; accountListId in both directions) — closes a mutation-proven hole
  3. Resolve the dead mocks prop either way
  4. Copy revision
  5. Defer: theme palette, doc drift, rules globs

Comment thread src/components/HrTools/NsGoalCalculator/GoalSettings/useMpdGoalPreview.ts Outdated
Comment thread src/components/HrTools/NsGoalCalculator/GoalSettings/GoalSettingsForm.tsx Outdated
Comment thread src/components/HrTools/NsGoalCalculator/GoalSettings/GoalSettingsWarning.tsx Outdated
Comment thread src/components/HrTools/NsGoalCalculator/GoalSettings/GoalSettingsWarning.tsx Outdated
Comment thread src/components/HrTools/NsGoalCalculator/GoalSettings/GoalSettingsWarning.test.tsx Outdated
Comment thread src/components/HrTools/NsGoalCalculator/GoalSettings/useMpdGoalPreview.ts Outdated
Comment thread src/components/HrTools/NsGoalCalculator/GoalSettings/useMpdGoalPreview.ts Outdated
Comment thread src/components/HrTools/NsGoalCalculator/GoalSettings/useMpdGoalPreview.ts Outdated
@zweatshirt
zweatshirt marked this pull request as ready for review July 21, 2026 14:55

@zweatshirt zweatshirt left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Multi-Agent Code Review — PR #1930 (Round 3)

Verdict: APPROVED WITH SUGGESTIONS — no blockers. Highest finding 6.0/10.

Risk: 7/10 (HIGH)src/components/**/*.graphql (+2), 4x feature components (+4), ~145 non-test lines (+1). Senior review recommended.

6 agents (Architecture, Testing, Standards, Data Integrity, UX, Financial Reporting). Third round on this branch; the six fix-up commits since the last review closed every prior finding, so this pass focuses on the new code.

Confirmed fixed since the last review

Verified by mutation testing — each item below now fails a targeted test when reverted:

Fix Guarded by
Warning flicker on every edit does not re-raise a resolved warning…
accountListId untested in both directions both useNewStaffGoalCalculation tests
Warning-in-form integration unasserted shows the over-cap warning beside the save actions
Real timers against a 500ms debounce fake timers added; probed with a 5,000,000ms debounce
Dead mocks test prop removed
Stale plural JSDoc, unused export fixed

Mutation audit: 9 of 10 mutations caught. The one gap is the .catch fallback — see the inline comment on useMpdGoalPreview.ts.

Root cause found for "the warning shows when Annual Requested Salary is empty"

This was reported during review and is not stale persistence. app/services/new_staff_goals/salary_calculation.rb:73-81:

requested_annual_salary = calculator_attribute(:requested_annual_salary)&.to_d
requested_annual_salary || ((geographic_multiplier * base) + tenure_adjustment + debt_salary)

An empty field maps to null on the client (toNumberOrNull('')), and the API then computes a default salary and compares that against the cap. The flag is correct — the goal calculation really does use that computed figure. The copy is what's wrong: it says "requested salary" when nothing was requested. See the inline comment on GoalSettingsWarning.tsx.

Findings on related files (not in this PR)

1. Contrast fix belongs in src/theme.ts, not in the component. palette.warning is never defined, so warning.main resolves to MUI's default #ed6c023.11:1 on #fff, failing the 4.5:1 bar for 14px body2. The repo's own statusWarning (#D34400) is 4.57:1. But HrTools uses warning.main in 7 of 7 pre-existing cases, so changing only this file would make it the sole outlier. Systemic fix:

warning: { main: statusColors.warning },  // #D34400 -> 4.57:1
success: { main: statusColors.success },  // CardTitle.tsx:24 has the same latent defect

Pre-existing and tree-wide — should not block this PR. Standalone ticket recommended.

2. Review-process gap. .claude/rules/code-review.md lists src/components/User/impersonate*, which matches nothing — impersonation lives under Settings/Admin/ImpersonateUser/, Settings/Organization/ImpersonateUser/, and ProfileMenu/. The Security agent's impersonation trigger never fires. Also stale: src/components/Reports/SalaryCalculator/** and pages/api/Schema/uploads/**.

3. Doc drift. CLAUDE.md and the rules file say "Material UI v5"; yarn.lock resolves @mui/material 7.3.11.

Questions for the API team (mpdx_api #3469)

  1. Expose the amounts behind the warning. To say "Total salary of $X is over the standard cap of $Y" the client needs combined_uncapped_total_salary and base_salary_cap on NewStaffGoalCalculationCalculations. Neither is exposed today, and the three salary fields that are (salary, salarySubtotal, totalSalary) are all monthly and post-cappingbase_salary scales down when the combined total exceeds the cap, so totalSalary is by construction never over the cap. Rendering any of them beside "is over the cap" would show a number that isn't. This is the blocker on improving the copy further.
  2. Seeding. salary_over_cap? calls base_salary_cap unconditionally, whereas salary_cap only reaches it when allow_salary_over_cap == 'no'. It resolves via MpdGoalMiscConstant.find_by!, which raises, and a raise would null the whole calculations block via non-null propagation — taking monthlyGoal with it. The in-repo seeder is all-or-nothing per year (so the risk is pre-existing, not new), but production seeding isn't in the repo. Is the prod path also all-or-nothing per year, and is every calculations_year in use backfilled?
  3. Seeded cap values differ from the spec. MpdGoalMiscConstantsSeeder has 80k/125k/90k/145k; goal_calculator_spec.rb stubs 85k. Which figures are current?

Comment thread src/components/HrTools/NsGoalCalculator/GoalSettings/useMpdGoalPreview.ts Outdated
Comment thread src/components/HrTools/NsGoalCalculator/GoalSettings/GoalSettingsWarning.tsx Outdated
Comment thread src/components/HrTools/NsGoalCalculator/GoalSettings/useMpdGoalPreview.ts Outdated
setPreview({
attributes: debounced,
monthlyGoal: null,
salaryOverCap: null,

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] **A failed preview silently erases a warning the user already raised — and nothing tests it.**

This .catch sets salaryOverCap: null, which line 152's ?? coalesces to savedSalaryOverCap. So an admin who raises the salary over cap, sees the warning, then makes any further edit whose preview request fails, watches the warning disappear — and can reasonably read that as "resolved". There is no snackbar (context: { suppressErrors: true }, line 108) and GoalSettingsWarning never consumes the hook's failed flag, so unlike MpdGoalPreview — which renders "Preview unavailable" for the same failure — the warning bar shows nothing at all.

This also contradicts the flicker fix's own principle: every other path holds the last known answer rather than reverting to saved.

Untested. Changing this null to false left 165 of 165 tests passing. It became unreachable from the suite when the mocks prop was removed from GoalSettingsWarning.test.tsx — that prop was the only handle for forcing a failure.

Two defensible resolutions:

// (a) hold the last known verdict rather than dropping it
.catch(() => {
  if (active) {
    setPreview((prev) => ({
      attributes: debounced,
      monthlyGoal: null,
      salaryOverCap: prev?.salaryOverCap ?? null,
    }));
  }
});

or (b) keep reverting to saved, but have GoalSettingsWarning render the failed state the way MpdGoalPreview does, so the disappearance is explained rather than silent.

Either way, add a test — re-introducing the mocks override (the pattern still present in MpdGoalPreview.test.tsx) is enough.

Severity is bounded because the warning is advisory, never gates Save, and self-corrects on the next successful preview or on save.

Comment thread src/components/HrTools/NsGoalCalculator/GoalSettings/GoalSettingsForm.test.tsx Outdated
Comment thread src/components/HrTools/NsGoalCalculator/GoalSettings/GoalSettingsWarning.tsx Outdated
savedSalaryOverCap = false,
previewSalaryOverCap = false,
}) => (
<GqlMockedProvider<{

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] **No `ThemeProvider`, unlike every sibling.**

11 test files in this directory wrap in <ThemeProvider theme={theme}>, including the closest analogue MpdGoalPreview.test.tsx:56-58.

GoalSettingsWarning.tsx:35-36 styles on warning.main, so in tests it resolves against MUI's default palette rather than src/theme. The suite therefore cannot catch a token that exists in the default theme but not the project's — which is precisely the situation here (palette.warning is undefined in src/theme.ts, see the review body).

Low blast radius, but it's the reason the contrast issue was invisible to CI.

Comment thread src/components/HrTools/NsGoalCalculator/GoalSettings/GoalSettingsWarning.test.tsx Outdated
Comment thread src/components/HrTools/NsGoalCalculator/GoalSettings/GoalSettingsForm.test.tsx Outdated
@canac

canac commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

@zweatshirt Are you also going to add warnings that the user is healthcare exempt, SECA exempt, or over the debt cap?

(Sorry I missed the debt cap before with the API PR. I haven't dug in, but that check may need to happen on the server.)

@zweatshirt

Copy link
Copy Markdown
Contributor Author

@canac I can add that to this PR. My brain assumed we weren't adding them due to what we concluded with the backend PR, but it makes sense we want them still.

@canac

canac commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

Yesterday, I was just thinking about the NSGC!DA5 checks, but it slipped my mind that we also need to implement NSGC!A33: IF(CV5<>"No","SECA Opted Out("&CV5&") - ","")&IF(CU5<>"No","Healthcare Exempt("&CU5&") - ","")&IF(CW5<>"No","Allow OverSalCap("&CW5&") - ","")&IF(CX5<>"No","Allow OverDebtCap("&CX5&") - ",""). Sorry about that!

@zweatshirt

Copy link
Copy Markdown
Contributor Author

@canac The backend PR added a boolean for healthcare exemption but you mentioned we don't need to check for it and so I removed it, are we still wanting to implement it?

@canac

canac commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

The sheet just surfaces a non-blocking warning when someone has an opt out or exemption. I think we can just read the booleans from the goal calculation and show a message if any of them are true. Like it says something like "SECA Opted Out (Both)", for example. I don't think that needs to be a validation in the backend calculations. What do you think?

@zweatshirt

zweatshirt commented Jul 21, 2026

Copy link
Copy Markdown
Contributor Author

@canac That was what I was intending with the boolean in the GraphQL for healthcare exemption. Is the goal to do as much of the logic for the warnings in the the frontend then? My idea was that we could send booleans to the frontend on which warnings to display and have the frontend act on those (or in the case of SECA we could maybe do an enum)

I guess the frontend has enough information for most of these apart from the salary cap and debt cap to do it in the frontend instead, though

@zweatshirt zweatshirt added the Staging API Run GraphQL codegen against the staging API label Jul 22, 2026
@zweatshirt

Copy link
Copy Markdown
Contributor Author

@canac This is basically ready for review. I've done multiple review agent iterations, it is considered critical. Sorry, it turned out to be a much bigger PR than I expected. Feel free to share constructive criticism.

@zweatshirt zweatshirt changed the title MPDX-9818 - Add salary over cap warning to goal settings and related tests MPDX-9818 - Add DA5 and A33 warnings/errors to goal settings Jul 22, 2026
@zweatshirt zweatshirt removed the Staging API Run GraphQL codegen against the staging API label Jul 22, 2026
@zweatshirt
zweatshirt requested review from canac and removed request for canac July 23, 2026 19:41
@zweatshirt

Copy link
Copy Markdown
Contributor Author

@canac Did you want me to redo this PR? I noticed it hadn't been reviewed in a while. It ended up being a big PR, and there are always areas to improve, so I am happy to rework it if needed.

@canac

canac commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

Oh sorry, for some reason I thought you were going to make more changes. I can look over it tomorrow if it's ready for review!

@zweatshirt zweatshirt left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 Multi-Agent Code Review — ✅ Approved with Suggestions

6 specialized reviewers (Architecture, Testing, Standards, Data Integrity, UX, Financial-domain) + dependency analysis. All returned High confidence with zero critical / high / important findings. What remains is a set of optional low-severity test-coverage and a11y-polish suggestions.

Scope note: this review deliberately excludes local commit e67821d21 (review-process / governance files) per the author's request. The reviewed tree is the remote PR head bb84087ac — the 16 goal-settings feature files — so it matches this PR's diff exactly.

Risk: 🟢 LOW

Axis Score Why
Blast Radius 1 Contained to GoalSettings/; one additive .graphql fragment (0 external dependents on every changed export)
Complexity 2 New warning rule-engine + shared preview state held across in-flight edits
Sensitivity 1 Displays financial figures; .graphql floor. No auth / PII / money-write
Recoverability 0 CI-gated frontend, fully revertible, no persisted side effects

effort 3 · danger 1 → auto-approve eligible.

Dependency impact

Blast radius genuinely contained. useMpdGoalPreview, MpdGoalPreview, GoalSettingsHeader, useGoalSettingsField, and all new exports have 0 external dependents; every in-directory call site was updated. The only cross-boundary change — adding salaryOverCap / debtOverCap to NewStaffGoalCalculationFields — is purely additive.

Highlights

  • Data integrity sound: previewing (a mutation) cannot drift the saved display — protected three ways (fetchPolicy: 'no-cache', distinct NewStaffGoalCalculationPreview typename, non-normalized value-object calculations).
  • N+1 removed: the shared provider collapses per-consumer preview requests into one (verified by "previews an edit exactly once with every consumer mounted").
  • A11y choices are deliberate & correct: aria-live="polite" + role="presentation" on the grouped alerts, and no aria-invalid on advisory outlines.
  • Money math safe: the single arithmetic line is the established cents-rounding idiom; no -$0.00 path.

Findings

8 open suggestions (all severity ≤ 5.0) posted inline. Nothing blocks merge.

Dismissed by author (acknowledged — not counted toward the verdict)

  • useMpdGoalPreview.ts (3.5) — held over-cap flag / transient header-vs-warning divergence during in-flight edits — intentional, documented behavior
  • GoalSettingsPreviewContext.tsx (3.0) — warnings rebuild each keystroke — intentional, documented behavior

Also: the prior sev-6.5 /dismissed finding ("every edit runs the goal calculation twice") is resolved by this PR's shared-provider refactor — no new finding matches it.

Security agent skipped (no api/auth/apollo-config/env/workflow/dependency changes in scope). Generated by a multi-agent review harness — findings are advisory.

initialTouched={initialTouched}
onSubmit={jest.fn()}
>
<GoalSettingsPreviewProvider

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] Every test in this file now renders inside `GoalSettingsPreviewProvider`, so the `useGoalSettingsPreview() === null` (no-provider) branch is no longer exercised *here*. It is still covered by the untouched field-component tests (`GoalSettingsTextField/Select/NumberField/YesNoField.test.tsx`), so this is a locality nit, not a coverage hole — optional: re-add one no-provider case asserting no outline is applied.

expect(getByText('Staff opted out of SECA')).toBeInTheDocument();
expect(queryByText('Both opted out of SECA')).not.toBeInTheDocument();
});
it('announces warnings politely, from a region that exists while empty', () => {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Medium] Untested error path. A **failed** preview leaves `previewSalaryOverCap`/`previewDebtOverCap` null, so the warning should revert to the *saved* over-cap flags. The goal-figure failure fallback is tested (`MpdGoalPreview.test.tsx`), but the warning-side fallback is not. Add a test with a rejecting `PreviewNewStaffGoalCalculation` mock + `savedSalaryOverCap` asserting the saved warning stays shown after an edit fails.

.filter(({ fields }) => fields.includes(name))
.map(({ severity }) => severity);

return severities.find((severity) => severity === 'error') ?? severities[0];

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] `severities.find((s) => s === 'error') ?? severities[0]` — the error-precedence branch is currently **unreachable**: `SALARY_CAP_FIELDS`, `SECA_FIELDS`, and `HEALTHCARE_FIELDS` are disjoint and the other warnings carry `fields: []`, so no field ever appears in two warnings and `severities.length` is always 0 or 1. Either add a direct unit test with a field in both an error and a warning to lock the precedence in, or note in-code that it is defensive.

return candidates.filter(({ active }) => active);
};

export const getFieldSeverity = (

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] This pure rule engine (`buildGoalSettingsWarnings` / `getFieldSeverity`) is only covered indirectly through three component test files. A colocated `goalSettingsWarnings.test.ts` exercising the DA5/A33 truth table directly would be cheaper, document the rules in one place, and satisfy the repo's 'colocated test per new lib function' checklist item. Non-blocking — behavior is well covered indirectly today.

showLabel?: boolean;
};

const outlineSx = (severity: WarningSeverity) => ({

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] The contributing-field outline is **color-only**. For salary/debt the warning text names the concept, but fields like `geographicLocation` and `contribution403bPercentage` (in `SALARY_CAP_FIELDS`) get no textual/icon/ARIA pointer — the tinted border is their sole cue. `aria-invalid` is correctly avoided (these are advisory, not validation errors), so this is an acceptable tradeoff, but a known a11y residual worth a follow-up if those fields need a non-color indicator.

<Alert
key={severity}
severity={severity}
role="presentation"

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] The error-vs-warning distinction is conveyed only visually (MUI's severity icon/color). The same text 'Total salary is over the standard cap' lands in the error or warning group depending on whether an exception is approved, but a screen-reader/colorblind user gets no cue to the allowed-vs-not distinction. The primary 'over cap' fact is fully in text, so low-priority polish.

const previewDebtOverCap = dirty ? (preview?.debtOverCap ?? null) : null;

return { calculating, displayGoal, diff, changed, failed };
return {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] The hook now returns `previewSalaryOverCap`/`previewDebtOverCap` (driving the A33/DA5 warnings), not just the MPD goal figure, and is consumed only by `GoalSettingsPreviewProvider`. The name `useMpdGoalPreview` reads narrower than its actual role — consider a rename (e.g. `useGoalPreview` / `useGoalSettingsPreviewData`). Purely cosmetic.

values.benefitsPlan !== MpdGoalBenefitsConstantPlanEnum.Select;

// Only reachable via `everyoneExempt`, which is either both spouses or a lone applicant
const staffName = values.firstName || t('Staff');

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] The `t('Staff')` / `t('Spouse')` name fallbacks (when `firstName`/`spouseFirstName` are empty) are never exercised — every exempt-plan test supplies real names. A one-line test with an empty name would cover them.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AI Review Auto-Approval

Risk Level: LOW (0/10)
Verdict: APPROVED_WITH_SUGGESTIONS (suggestions posted, no blockers)

This PR was auto-approved because:

  • The multi-agent AI review determined it is low risk
  • No blocking issues were found
  • All suggestions have been posted as review comments for the developer to consider
  • 3 non-blocking finding(s) were reviewed and dismissed by the developer

If you believe this PR needs human review, dismiss this approval and request a review manually.

@zweatshirt

Copy link
Copy Markdown
Contributor Author

@canac I can finish up the code given the agent review (sorry, I know it's produced a lot of comments, I wanted to test the new review scoring here). If you are up for it, I was mostly hoping for a human look at the UI.

@canac canac left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't really know what stakeholders want for validation, but this seems like a great start! Hopefully we can get more feedback at some point.

Highlighting the fields that contribute to the goal is a really cool idea!

// Discrepancy with the sheet: DA5's plan list has no "Exempt" option, so it
// never defined this case. Following it literally makes Exempt a violating
// plan. Revisit with whoever owns the plan list if that reading is wrong.
const exemptPlanMismatch =

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't need this check anymore on the frontend either.

Comment on lines +164 to +166
calculationId={calculation.id}
savedSalaryOverCap={calculation.calculations.salaryOverCap}
savedDebtOverCap={calculation.calculations.debtOverCap}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What if we passed the full calculation to the provider and then it can read whichever fields it needs?

Suggested change
calculationId={calculation.id}
savedSalaryOverCap={calculation.calculations.salaryOverCap}
savedDebtOverCap={calculation.calculations.debtOverCap}
calculation={calculation}

: Math.round((previewGoal - savedMonthlyGoal) * 100) / 100;
const changed = Math.abs(diff) >= CENT_EPSILON;
const previewSalaryOverCap = dirty ? (preview?.salaryOverCap ?? null) : null;
const previewDebtOverCap = dirty ? (preview?.debtOverCap ?? null) : null;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we're adding more fields to the preview, maybe we should add the calculated 403b amounts too?

'contribution403bPercentage',
'spouseContribution403bPercentage',
// Sets the cost-of-living multiplier, and so the cap itself.
'geographicLocation',

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can leave this and see what stakeholders think if you want, but I'm thinking that we shouldn't mark this as invalid since it can't be changed (as long as it's correct). Age, tenure, and marital status also affect the salary cap, but we don't mark those fields as invalid.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Preview Environment Add this label to create an Amplify Preview

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants